/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package com.taobao.weex.utils;

import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import android.text.TextUtils;

import com.taobao.weex.IWXStatisticsListener;
import com.taobao.weex.WXEnvironment;
import com.taobao.weex.adapter.IWXSoLoaderAdapter;
import com.taobao.weex.adapter.IWXUserTrackAdapter;
import com.taobao.weex.common.WXErrorCode;

import dalvik.system.PathClassLoader;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Enumeration;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;


/**
 * Utility class for managing so library, including load native library and version management.
 * <ol>
 *   <li>Load library<br>
 *     It Will try to use {@link System#loadLibrary(String)} to load native library. If it successes,
 *     the Android Framework will be responsible for managing library and library version.
 *     If it fails in case of some ceratin armebi-v7a architecture device, it will try to extract
 *     native library from apk and copy it the data directory of the app. Then load it using
 *     {@link System#load(String)}.
 *     </li>
 *  <li>
 *       Version control for extracting native library from apk.
 *  </li>
 * </ol>
 */
public class WXSoInstallMgrSdk {

    final static String LOGTAG = "INIT_SO";
    //below is the CPU string types
    private final static String ARMEABI = "armeabi"; //default
    private final static String X86 = "x86";
    private final static String MIPS = "mips";
    private final static String STARTUPSO = "/libweexjsb.so";
    private final static String STARTUPSOANDROID15 = "/libweexjst.so";

    static Application mContext = null;
    private static IWXSoLoaderAdapter mSoLoader = null;
    private static IWXStatisticsListener mStatisticsListener = null;

    private static String mAbi = null;

    public static void init(Application c,
                            IWXSoLoaderAdapter loader,
                            IWXStatisticsListener listener) {
        mContext = c;
        mSoLoader = loader;
        mStatisticsListener = listener;
    }

    public static boolean isX86() {
        String cpuType = _cpuType();
        return cpuType.equalsIgnoreCase(X86);
    }

    public static boolean isCPUSupport() {
        String cpuType = _cpuType();
        return !cpuType.equalsIgnoreCase(MIPS);
    }

    /**
     * Load so library.
     * <p>
     * If a library loader adapter exists, use this adapter to load library,
     * otherwise use {@link System#loadLibrary(String)} to load library.
     * If failed to load library, try to extract the so library and load it
     * from arembi in the .apk
     *
     * @param libName library name, like webp, not necessary to be libwep.so
     * @param version the version of the so library
     */
    public static boolean initSo(String libName, int version, IWXUserTrackAdapter utAdapter) {
        String cpuType = _cpuType();
        if (cpuType.equalsIgnoreCase(MIPS)) {
            WXExceptionUtils.commitCriticalExceptionRT(null,
                    WXErrorCode.WX_KEY_EXCEPTION_SDK_INIT,
                    "initSo", "[WX_KEY_EXCEPTION_SDK_INIT_CPU_NOT_SUPPORT] for android cpuType is MIPS",
                    null);
            return false;
        }

        // copy startup so
        if (WXEnvironment.CORE_SO_NAME.equals(libName)) {
            copyStartUpSo();
        }


        boolean InitSuc = false;
        try {
            // If a library loader adapter exists, use this adapter to load library
            // instead of System.loadLibrary.
            if (mSoLoader != null) {
                mSoLoader.doLoadLibrary("c++_shared");
            } else {
                System.loadLibrary("c++_shared");
            }
        } catch (Exception | Error e) {
            WXExceptionUtils.commitCriticalExceptionRT(null,
                    WXErrorCode.WX_KEY_EXCEPTION_SDK_INIT,
                    "initSo",
                    "load c++_shared failed Detail Error is: " + e.getMessage(),
                    null);

            if (WXEnvironment.isApkDebugable()) {
                throw e;
            }
        }

        /**
         * Load library with {@link System#loadLibrary(String)}
         */
        try {
            // If a library loader adapter exists, use this adapter to load library
            // instead of System.loadLibrary.
            if (mSoLoader != null) {
                mSoLoader.doLoadLibrary(libName);
            } else {
                System.loadLibrary(libName);
            }

            InitSuc = true;
        } catch (Exception | Error e2) {
            if (cpuType.contains(ARMEABI) || cpuType.contains(X86)) {
                WXExceptionUtils.commitCriticalExceptionRT(null,
                        WXErrorCode.WX_KEY_EXCEPTION_SDK_INIT,
                        "initSo", libName + "[WX_KEY_EXCEPTION_SDK_INIT_CPU_NOT_SUPPORT] for android cpuType is " + cpuType +
                                "\n Detail Error is: " + e2.getMessage(),
                        null);
            }

            if (WXEnvironment.isApkDebugable()) {
                throw e2;
            }
            InitSuc = false;
        }

//      try {

//        if (!InitSuc) {
//
//          //File extracted from apk already exists.
//          if (isExist(libName, version)) {
//            boolean res = _loadUnzipSo(libName, version, utAdapter);
//            if (res) {
//              return res;
//            } else {
//              //Delete the corrupt so library, and extract it again.
//              removeSoIfExit(libName, version);
//            }
//          }
//
//          //Fail for loading file from libs, extract so library from so and load it.
//          if (cpuType.equalsIgnoreCase(MIPS)) {
//            return false;
//          } else {
//            try {
//              InitSuc = unZipSelectedFiles(libName, version, utAdapter);
//            } catch (IOException e2) {
//              e2.printStackTrace();
//            }
//          }
//
//        }
//      } catch (Exception | Error e) {
//        InitSuc = false;
//        e.printStackTrace();
//      }
        //  }
        return InitSuc;
    }

    private static File _desSoCopyFile(String soName) {
        String cpuType = _cpuType();
        String copyPath = WXEnvironment.copySoDesDir();
        if (TextUtils.isEmpty(copyPath)) {
            return null;
        }
        File desDir = new File(copyPath, soName + "/" + cpuType);
        return desDir;
    }

    /**
     * copyStartUpSo
     */
    @SuppressLint("SdCardPath")
    public static void copyStartUpSo() {
        try {
            // copy libjsb.so to cache/weex/jsb/cputype
            String pkgName = WXEnvironment.getApplication().getPackageName();
            String cacheFile = WXEnvironment.getApplication().getApplicationContext().getCacheDir().getPath();

            // cp weexjsb any way
            // if android api < 16 copy libweexjst.so else copy libweexjsb.so
            boolean pieSupport = true;
            File newfile;
            String startSoName = WXEnvironment.CORE_JSB_SO_NAME;
            String startSoPath = STARTUPSO;
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
                pieSupport = false;
                startSoName = WXEnvironment.CORE_JST_SO_NAME;
                startSoPath = STARTUPSOANDROID15;
            }

            final File copyPath = _desSoCopyFile(startSoName);
            if (!copyPath.exists()) {
                copyPath.mkdirs();
            }
            newfile = new File(copyPath, startSoPath);
            WXEnvironment.CORE_JSB_SO_PATH = newfile.getAbsolutePath();
            String jsb = WXEnvironment.getDefaultSettingValue(startSoName, "-1");
            if (newfile.exists() && TextUtils.equals(WXEnvironment.getAppVersionName(), jsb)) {
                // no update so skip copy
                return;
            }

            String path = "/data/data/" + pkgName + "/lib";
            if (cacheFile != null && cacheFile.indexOf("/cache") > 0) {
                path = cacheFile.replace("/cache", "/lib");
            }
            File oldfile = null;
            if (pieSupport) {
                oldfile = new File(path, STARTUPSO);
            } else {
                oldfile = new File(path, STARTUPSOANDROID15);
            }


            if (!oldfile.exists()) {
                try {
                    String weexjsb = ((PathClassLoader) (WXSoInstallMgrSdk.class.getClassLoader())).findLibrary(startSoName);
                    oldfile = new File(weexjsb);
                } catch (Throwable throwable) {
                    // do nothing
                }
            }


            if (!oldfile.exists()) {
                String extractSoPath = WXEnvironment.extractSo();
                oldfile = new File(extractSoPath, STARTUPSO);
            }

            if (oldfile.exists()) {
                WXFileUtils.copyFile(oldfile, newfile);
            }
            WXEnvironment.writeDefaultSettingsValue(startSoName, WXEnvironment.getAppVersionName());
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    public static void copyJssRuntimeSo() {
        boolean tryUseRunTimeApi = WXUtils.checkGreyConfig("wxapm", "use_runtime_api", "0");
        WXLogUtils.e("weex", "tryUseRunTimeApi ? " + tryUseRunTimeApi);
        if (!tryUseRunTimeApi) {
            return;
        }
        try {
            WXLogUtils.e("weex", "copyJssRuntimeSo: ");
            File toPath = _desSoCopyFile(WXEnvironment.CORE_JSS_SO_NAME);
            if (!toPath.exists()) {
                toPath.mkdirs();
            }
            File targetFile = new File(toPath, "libweexjss.so");

            /** 1. check so and versionCode. if update, then rm old jss.so(runtime) in pkg/libs, and copy new so from apk **/
            String keyVersionCode = "app_version_code_weex";
            String defaultSettingValue = WXEnvironment.getDefaultSettingValue(keyVersionCode, "-1");


            if (targetFile.exists()) {
                if (!TextUtils.equals(WXEnvironment.getAppVersionName(), defaultSettingValue)) {
                    targetFile.delete();
                } else {
                    WXEnvironment.CORE_JSS_SO_PATH = targetFile.getAbsolutePath();
                    WXEnvironment.sUseRunTimeApi = true;
                    WXLogUtils.e("weex", "copyJssRuntimeSo exist:  return");
                    return;
                }
            }
            /** 2. copy jss(runtime) so **/
            String fromPath = ((PathClassLoader) (WXSoInstallMgrSdk.class.getClassLoader())).findLibrary("weexjssr");
            if (TextUtils.isEmpty(fromPath)) {
                return;
            }
            targetFile.createNewFile();
            WXFileUtils.copyFileWithException(new File(fromPath), targetFile);
            /**3. update flag **/
            WXEnvironment.CORE_JSS_SO_PATH = targetFile.getAbsolutePath();
            WXEnvironment.writeDefaultSettingsValue(keyVersionCode, WXEnvironment.getAppVersionName());
            WXEnvironment.sUseRunTimeApi = true;
            WXLogUtils.e("weex", "copyJssRuntimeSo: cp end and return ");
        } catch (Throwable e) {
            e.printStackTrace();
            WXEnvironment.sUseRunTimeApi = false;
            WXLogUtils.e("weex", "copyJssRuntimeSo:  exception" + e);
        }
    }

    private static String _getFieldReflectively(Build build, String fieldName) {
        try {
            final Field field = Build.class.getField(fieldName);
            return field.get(build).toString();
        } catch (Exception ex) {
            return "Unknown";
        }
    }

    public static String _cpuType() {
        if (TextUtils.isEmpty(mAbi)) {
            try {
                mAbi = Build.CPU_ABI;
            } catch (Throwable e) {
                e.printStackTrace();
                mAbi = ARMEABI;
            }
            if (TextUtils.isEmpty(mAbi)) {
                mAbi = ARMEABI;
            }
            mAbi = mAbi.toLowerCase(Locale.ROOT);
        }
        return mAbi;
    }

    /**
     * @param libName lib name
     * @param size    the right size of lib
     * @return true for valid  ; false for InValid
     */
    static boolean checkSoIsValid(String libName, long size) {
        Context context = mContext;
        if (null == context) {
            return false;
        }
        try {
            long start = System.currentTimeMillis();
            if (WXSoInstallMgrSdk.class.getClassLoader() instanceof PathClassLoader) {

                String path = ((PathClassLoader) (WXSoInstallMgrSdk.class.getClassLoader())).findLibrary(libName);
                if (TextUtils.isEmpty(path)) {
                    return false;
                }
                File file = new File(path);

                if (!file.exists() || size == file.length()) {
                    WXLogUtils.w("weex so size check path :" + path + "   " + (System.currentTimeMillis() - start));
                    return true;
                } else {
                    return false;
                }
            }
        } catch (Throwable e) {
            WXExceptionUtils.commitCriticalExceptionRT(null,
                    WXErrorCode.WX_KEY_EXCEPTION_SDK_INIT,
                    "checkSoIsValid", "[WX_KEY_EXCEPTION_SDK_INIT_CPU_NOT_SUPPORT] for " +
                            "weex so size check fail exception :" + e.getMessage(),
                    null);
            WXLogUtils.e("weex so size check fail exception :" + e.getMessage());
        }

        return true;
    }

    /**
     * Concatenate the path of the so library, including directory.
     *
     * @param libName the raw name of the lib
     * @param version the version of the so library
     * @return the path of the so library
     */
    @SuppressLint("SdCardPath")
    static String _targetSoFile(String libName, int version) {
        Context context = mContext;
        if (null == context) {
            return "";
        }

        String path = "/data/data/" + context.getPackageName() + "/files";

        File f = context.getFilesDir();
        if (f != null) {
            path = f.getPath();
        }
        return path + "/lib" + libName + "bk" + version + ".so";

    }

    /**
     * Remove the so library if it had been extracted.
     *
     * @param libName
     * @param version
     */
    static void removeSoIfExit(String libName, int version) {

        String file = _targetSoFile(libName, version);
        File a = new File(file);
        if (a.exists()) {
            a.delete();
        }

    }

    /**
     * Tell whether the so is extracted.
     */
    static boolean isExist(String libName, int version) {

        String file = _targetSoFile(libName, version);
        File a = new File(file);
        return a.exists();

    }


    /**
     * Load .so library
     */
    @SuppressLint("UnsafeDynamicallyLoadedCode")
    static boolean _loadUnzipSo(String libName,
                                int version,
                                IWXUserTrackAdapter utAdapter) {
        boolean initSuc = false;
        try {
            if (isExist(libName, version)) {
                // If a library loader adapter exists, use this adapter to load library
                // instead of System.load.
                if (mSoLoader != null) {
                    mSoLoader.doLoad(_targetSoFile(libName, version));
                } else {
                    System.load(_targetSoFile(libName, version));
                }
            }
            initSuc = true;
        } catch (Throwable e) {
            initSuc = false;
            WXExceptionUtils.commitCriticalExceptionRT(null,
                    WXErrorCode.WX_KEY_EXCEPTION_SDK_INIT_CPU_NOT_SUPPORT,
                    "_loadUnzipSo", "[WX_KEY_EXCEPTION_SDK_INIT_WX_ERR_COPY_FROM_APK] " +
                            "\n Detail Msg is : " + e.getMessage(),
                    null);
            WXLogUtils.e("", e);
        }
        return initSuc;
    }

    static boolean unZipSelectedFiles(String libName,
                                      int version,
                                      IWXUserTrackAdapter utAdapter) throws ZipException, IOException {
        String sourcePath = "lib/armeabi/lib" + libName + ".so";

        String zipPath = "";
        Context context = mContext;
        if (context == null) {
            return false;
        }

        ApplicationInfo aInfo = context.getApplicationInfo();
        if (null != aInfo) {
            zipPath = aInfo.sourceDir;
        }

        ZipFile zf;
        zf = new ZipFile(zipPath);
        try {

            for (Enumeration<?> entries = zf.entries(); entries.hasMoreElements(); ) {
                ZipEntry entry = ((ZipEntry) entries.nextElement());
                if (entry.getName().startsWith(sourcePath)) {

                    InputStream in = null;
                    FileOutputStream os = null;
                    FileChannel channel = null;
                    int total = 0;
                    try {

                        //Make sure the old library is deleted.
                        removeSoIfExit(libName, version);

                        //Copy file
                        in = zf.getInputStream(entry);
                        os = context.openFileOutput("lib" + libName + "bk" + version + ".so",
                                Context.MODE_PRIVATE);
                        channel = os.getChannel();

                        byte[] buffers = new byte[1024];
                        int realLength;

                        while ((realLength = in.read(buffers)) > 0) {
                            //os.write(buffers);
                            channel.write(ByteBuffer.wrap(buffers, 0, realLength));
                            total += realLength;

                        }
                    } finally {
                        if (in != null) {
                            try {
                                in.close();
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }

                        if (channel != null) {
                            try {
                                channel.close();
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }

                        if (os != null) {
                            try {
                                os.close();
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }

                        if (zf != null) {
                            zf.close();
                            zf = null;
                        }
                    }

                    if (total > 0) {
                        return _loadUnzipSo(libName, version, utAdapter);
                    } else {
                        return false;
                    }
                }
            }
        } catch (java.io.IOException e) {
            e.printStackTrace();
            WXExceptionUtils.commitCriticalExceptionRT(null,
                    WXErrorCode.WX_KEY_EXCEPTION_SDK_INIT_CPU_NOT_SUPPORT,
                    "unZipSelectedFiles", "[WX_KEY_EXCEPTION_SDK_INIT_unZipSelectedFiles] " +
                            "\n Detail msg is: " + e.getMessage(),
                    null);

        } finally {

            if (zf != null) {
                zf.close();
                zf = null;
            }
        }
        return false;
    }

    /**
     * Using {@Code WXExceptionUtils.commitCriticalExceptionRT}  insted
     */
//  static void commit(IWXUserTrackAdapter utAdapter, String errCode, String errMsg) {
//    if (mStatisticsListener != null) {
//      mStatisticsListener.onException("0", errCode, errMsg);
//    }
//
//    if (utAdapter == null) {
//      return;
//    }
//    if (errCode != null && errMsg != null) {
//      WXPerformance p = new WXPerformance();
//      p.errCode = errCode;
//      p.errMsg = errMsg;
//      utAdapter.commit(null, null, WXEnvironment.ENVIRONMENT, p, null);
//    } else {
//      utAdapter.commit(null, null, WXEnvironment.ENVIRONMENT, null, null);
//
//    }
//  }

}
